/**
 * 版权所有(C)，上海海鼎信息工程股份有限公司，2007，所有权利保留。
 * 
 * 项目名：	rumba
 * 文件名：	WeeklySchedule.java
 * 模块说明：	
 * 修改历史：
 * Jun 18, 2007 - lxm - 创建。
 */
package com.hd123.rumba.runtime.backprocess;

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashSet;

import com.hd123.rumba.exception.BusinessException;
import com.hd123.rumba.util.DateUtil;

/**
 * 每周后台任务调度 | 实现类
 * <p>
 * 
 * 调度公式：从{@link #startDate}到{@link #finishDate}期间，每间隔{@link #every}周{@link validDOWs}指定一周中的某天（Day-of-week，可多选）的指定时间{@link #time}被触发。
 * <p>
 * 
 * 关于周期的计算，把startDate所在周作为第一周，将其后的时间按照every指定的周数分割为若干段，每个段的第一周中指定的一周中的某天的time指定时间将被触发。
 * 
 * @author lxm
 * 
 */
public class WeeklySchedule implements ITaskSchedule {

  private Date time;
  private int every = 1;
  private Date startDate;
  private Date finishDate = null;
  private HashSet<Integer> daysOfWeek = new HashSet<Integer>();

  public WeeklySchedule() {
    super();
    this.time = DateUtil.getTimePart(new Date());
    this.startDate = DateUtil.truncate(new Date(), Calendar.DATE);
  }

  /** 属性：在一天中的时间。将忽略日期部分。 */
  public Date getTime() {
    return time;
  }

  public void setTime(Date time) throws BusinessException {
    assert time != null;
    if (time == null)
      throw new BusinessException("属性time不允许为null。");
    this.time = DateUtil.getTimePart(time);
  }

  /**
   * 设置在一天中的时间。
   * 
   * @param hour
   *          指定小时部分，24小时制。
   * @param minute
   *          指定分钟部分。
   * @param second
   *          指定秒部分。
   */
  public void setTime(int hour, int minute, int second) {
    Calendar c = Calendar.getInstance();
    c.clear();
    c.set(Calendar.HOUR_OF_DAY, hour);
    c.set(Calendar.MINUTE, minute);
    c.set(Calendar.SECOND, second);
    this.time = c.getTime();
  }

  /** 间隔周数。默认为1，&gt;0。 */
  public int getEvery() {
    return every;
  }

  /** 间隔周数。默认为1，&gt;0。 */
  public void setEvery(int every) throws BusinessException {
    if (every <= 0)
      throw new BusinessException("属性every必须为大于等于1的整数。");
    this.every = every;
  }

  /** 结束日期。包括这一天。允许null，表示不指定结束日期。 */
  public Date getFinishDate() {
    return finishDate;
  }

  /** 结束日期。包括这一天。允许null，表示不指定结束日期。 */
  public void setFinishDate(Date finishDate) {
    this.finishDate = DateUtil.truncate(finishDate, Calendar.DATE);
  }

  /** 开始日期 */
  public Date getStartDate() {
    return startDate;
  }

  /** 开始日期 */
  public void setStartDate(Date startDate) {
    this.startDate = DateUtil.truncate(startDate, Calendar.DATE);
  }

  /**
   * 取得一周中指定日期是否触发。
   * 
   * @param dayOfWeek
   *          指定一周中指定日期。可选值为：{@link Calendar#SUNDAY}, ...,
   *          {@link Calendar#SATURDAY}
   * @return 返回是否触发。
   */
  public boolean getValid(int dayOfWeek) {
    assert (dayOfWeek >= Calendar.SUNDAY && dayOfWeek <= Calendar.SATURDAY);
    if (dayOfWeek < Calendar.SUNDAY || dayOfWeek > Calendar.SATURDAY)
      return false;
    return daysOfWeek.contains(Integer.valueOf(dayOfWeek));
  }

  /**
   * 设置一周中指定日期是否触发。
   * 
   * @param dayOfWeek
   *          指定一周中指定日期。可选值为：{@link Calendar#SUNDAY}, ...,
   *          {@link Calendar#SATURDAY}
   * @param valid
   *          指定是否触发。
   * @throws BusinessException
   */
  public void setValid(int dayOfWeek, boolean valid) throws BusinessException {
    assert (dayOfWeek >= Calendar.SUNDAY && dayOfWeek <= Calendar.SATURDAY);
    if (dayOfWeek < Calendar.SUNDAY || dayOfWeek > Calendar.SATURDAY)
      throw new BusinessException("未知一周中的日子\"" + dayOfWeek + "\"。");

    if (valid)
      daysOfWeek.add(Integer.valueOf(dayOfWeek));
    else
      daysOfWeek.remove(Integer.valueOf(dayOfWeek));
  }

  private static final String[] CAPTION_DAYOFWEEK = {
      "星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六" };
  private static final SimpleDateFormat sdfTime = new SimpleDateFormat("HH:mm:ss");
  private static final SimpleDateFormat sdfDate = new SimpleDateFormat("yyyy-MM-dd");

  public String getDescription() throws BusinessException {
    StringBuffer sb = new StringBuffer();
    sb.append("从");
    sb.append(sdfDate.format(this.startDate));
    sb.append("开始");
    if (this.finishDate != null) {
      sb.append("到");
      sb.append(sdfDate.format(this.finishDate));
      sb.append("期间");
    }
    if (this.every == 1)
      sb.append("每周");
    else {
      sb.append("每");
      sb.append(this.every);
      sb.append("周");
    }
    boolean first = true;
    for (int dayOfWeek : daysOfWeek) {
      if (!first)
        sb.append("、");
      sb.append(CAPTION_DAYOFWEEK[dayOfWeek]);
      first = false;
    }
    sb.append("的");
    sb.append(sdfTime.format(this.time));
    sb.append("执行。");
    return sb.toString();
  }

  public Date getNextTime() throws BusinessException {
    return getNextTime(new Date());
  }

  private static final long MILLISECONDS_OF_DAY = 24 * 60 * 60 * 1000;
  private static final long DAYS_OF_WEEK = 7;

  public Date getNextTime(Date baseTime) throws BusinessException {
    assert this.daysOfWeek.size() != 0;
    if (this.daysOfWeek.size() == 0)
      throw new BusinessException("无效的设置，一周中至少要有一天被选择。");

    if (baseTime == null)
      baseTime = new Date();

    // 找到baseTime所在周期时间段（根据every进行分段）的第一天（即第一个星期日），作为后面计算的一个基准日期。
    Date startFirstDOW = DateUtil.truncate(this.startDate, Calendar.WEEK_OF_MONTH);
    long nStartTime = startFirstDOW.getTime();
    long nBaseTime = baseTime.getTime();
    int nSecs = 0;
    if (nBaseTime >= nStartTime)
      nSecs = (int) Math.floor((nBaseTime - nStartTime)
          / ((double) this.every * DAYS_OF_WEEK * MILLISECONDS_OF_DAY));
    Date firstDate = DateUtil.add(startFirstDOW, Calendar.DATE,
        (int) (nSecs * this.every * DAYS_OF_WEEK));

    Date nextTime = null;
    while (nextTime == null) {
      // 搜索基准日期所在一周的每一天。
      for (int dow = 1; dow <= Calendar.SATURDAY; dow++) {
        if (!this.getValid(dow))
          continue;
        Date date = DateUtil.add(firstDate, Calendar.DATE, dow - 1);
        Date datetime = DateUtil.addDateAndTime(date, this.time);
        if (datetime.after(this.startDate) && datetime.after(baseTime)) {
          nextTime = datetime;
          break;
        }
      }
      // 调整基准日到下一个周期时间段的第一天。
      firstDate = DateUtil.add(firstDate, Calendar.DATE, (int) (this.every * DAYS_OF_WEEK));
    }
    // 结束日期检查
    if (this.finishDate != null
        && !nextTime.before(DateUtil.add(this.finishDate, Calendar.DATE, 1)))
      nextTime = null;
    return nextTime;
  }

  /**
   * 判断指定的时间是否是某个触发时间。
   * 
   * @param aTime
   *          指定的时间。
   * @return
   */
  public boolean isValid(Date aTime) {
    if (aTime == null)
      return false;

    // 检查是否在开始与结束日期之间
    if (aTime.before(this.startDate))
      return false;
    if (this.finishDate != null && !aTime.before(DateUtil.add(this.finishDate, Calendar.DATE, 1)))
      return false;

    // 检查时间部分
    if (DateUtil.getTimePart(aTime).compareTo(this.time) != 0)
      return false;

    // 检查星期数是否合法。
    int dow = DateUtil.get(aTime, Calendar.DAY_OF_WEEK);
    if (!this.getValid(dow))
      return false;

    // 检查是否是分段的第一周。
    Date aFirstDOW = DateUtil.truncate(aTime, Calendar.WEEK_OF_MONTH);
    Date startFirstDOW = DateUtil.truncate(this.startDate, Calendar.WEEK_OF_MONTH);
    long nDeltaWeeks = (aFirstDOW.getTime() - startFirstDOW.getTime())
        / (DAYS_OF_WEEK * MILLISECONDS_OF_DAY);
    double secs = nDeltaWeeks / (double) this.every;
    return Math.floor(secs) == secs;
  }

}
